1pub mod acp;
15pub mod core;
16pub mod health;
17pub mod persistent;
18pub mod pidregistry;
19pub mod process_tree;
20pub mod session_recovery;
21pub mod session_stats;
22pub mod shell;
23pub mod subprocess;
24pub mod watchdog;
25
26use std::any::Any;
27use std::collections::HashMap;
28use std::sync::{Arc, RwLock};
29
30use serde::{Deserialize, Serialize};
31
32use super::eventbus::EventBus;
33use super::storage::filestore::FileStore;
34use super::storage::store::Store;
35use super::obj::{Block, MetaMapType, TermSize};
36use super::wps::Broker;
37
38pub const STATUS_INIT: &str = "init";
41pub const STATUS_RUNNING: &str = "running";
42pub const STATUS_DONE: &str = "done";
43
44pub const BLOCK_CONTROLLER_SHELL: &str = "shell";
47pub const BLOCK_CONTROLLER_CMD: &str = "cmd";
48pub const BLOCK_CONTROLLER_TSUNAMI: &str = "tsunami";
49pub const BLOCK_CONTROLLER_SUBPROCESS: &str = "subprocess";
50pub const BLOCK_CONTROLLER_PERSISTENT: &str = "persistent";
51pub const BLOCK_CONTROLLER_ACP: &str = "acp";
52
53pub const META_KEY_CONTROLLER: &str = "controller";
56pub const META_KEY_CONNECTION: &str = "connection";
57pub const META_KEY_CMD: &str = "cmd";
58pub const META_KEY_CMD_CWD: &str = "cmd:cwd";
59#[allow(dead_code)]
60pub const META_KEY_CMD_SHELL: &str = "cmd:shell";
61pub const META_KEY_CMD_ARGS: &str = "cmd:args";
62pub const META_KEY_CMD_ENV: &str = "cmd:env";
63#[allow(dead_code)]
64pub const META_KEY_CMD_JWT: &str = "cmd:jwt";
65pub const META_KEY_CMD_RUN_ON_START: &str = "cmd:runonstart";
66pub const META_KEY_CMD_RUN_ONCE: &str = "cmd:runonce";
67pub const META_KEY_CMD_CLEAR_ON_START: &str = "cmd:clearonstart";
68pub const META_KEY_CMD_CLOSE_ON_EXIT: &str = "cmd:closeonexit";
69pub const META_KEY_CMD_CLOSE_ON_EXIT_FORCE: &str = "cmd:closeonexitforce";
70pub const META_KEY_CMD_CLOSE_ON_EXIT_DELAY: &str = "cmd:closeonexitdelay";
71#[allow(dead_code)]
72pub const META_KEY_CMD_INIT_SCRIPT: &str = "cmd:initscript";
73#[allow(dead_code)]
74pub const META_KEY_CMD_INIT_SCRIPT_BASH: &str = "cmd:initscript.bash";
75#[allow(dead_code)]
76pub const META_KEY_CMD_INIT_SCRIPT_ZSH: &str = "cmd:initscript.zsh";
77#[allow(dead_code)]
78pub const META_KEY_CMD_INIT_SCRIPT_FISH: &str = "cmd:initscript.fish";
79#[allow(dead_code)]
80pub const META_KEY_CMD_INIT_SCRIPT_PWSH: &str = "cmd:initscript.pwsh";
81#[allow(dead_code)]
82pub const META_KEY_TERM_LOCAL_SHELL_PATH: &str = "term:localshellpath";
83#[allow(dead_code)]
84pub const META_KEY_TERM_LOCAL_SHELL_OPTS: &str = "term:localshellopts";
85
86#[allow(dead_code)]
90pub const DEFAULT_TIMEOUT_MS: u64 = 2000;
91
92#[allow(dead_code)]
94pub const DEFAULT_GRACEFUL_KILL_WAIT_MS: u64 = 400;
95
96#[derive(Debug, Clone)]
101pub struct BlockInputUnion {
102 pub input_data: Option<Vec<u8>>,
104 pub sig_name: Option<String>,
106 pub term_size: Option<TermSize>,
108}
109
110impl BlockInputUnion {
111 pub fn data(data: Vec<u8>) -> Self {
112 Self {
113 input_data: Some(data),
114 sig_name: None,
115 term_size: None,
116 }
117 }
118
119 pub fn signal(name: &str) -> Self {
120 Self {
121 input_data: None,
122 sig_name: Some(name.to_string()),
123 term_size: None,
124 }
125 }
126
127 pub fn resize(size: TermSize) -> Self {
128 Self {
129 input_data: None,
130 sig_name: None,
131 term_size: Some(size),
132 }
133 }
134}
135
136fn is_false(v: &bool) -> bool {
137 !v
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, Default)]
144pub struct BlockControllerRuntimeStatus {
145 pub blockid: String,
146 #[serde(default)]
147 pub version: i32,
148 #[serde(default, skip_serializing_if = "String::is_empty")]
149 pub shellprocstatus: String,
150 #[serde(default, skip_serializing_if = "String::is_empty")]
151 pub shellprocconnname: String,
152 #[serde(default)]
153 pub shellprocexitcode: i32,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub spawn_ts_ms: Option<i64>,
157 #[serde(default, skip_serializing_if = "is_false")]
159 pub is_agent_pane: bool,
160}
161
162pub trait Controller: Send + Sync {
167 fn start(
170 &self,
171 block_meta: MetaMapType,
172 rt_opts: Option<serde_json::Value>,
173 force: bool,
174 ) -> Result<(), String>;
175
176 fn stop(&self, graceful: bool, new_status: &str) -> Result<(), String>;
179
180 fn get_runtime_status(&self) -> BlockControllerRuntimeStatus;
182
183 fn send_input(&self, input: BlockInputUnion, seq: Option<u64>) -> Result<(), String>;
186
187 fn controller_type(&self) -> &str;
189
190 #[allow(dead_code)]
192 fn block_id(&self) -> &str;
193
194 fn as_any(&self) -> &dyn Any;
196}
197
198static CONTROLLER_REGISTRY: std::sync::LazyLock<RwLock<HashMap<String, Arc<dyn Controller>>>> =
203 std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
204
205pub fn get_controller(block_id: &str) -> Option<Arc<dyn Controller>> {
207 CONTROLLER_REGISTRY
208 .read()
209 .unwrap()
210 .get(block_id)
211 .cloned()
212}
213
214pub fn register_controller(block_id: &str, controller: Arc<dyn Controller>) {
216 let mut registry = CONTROLLER_REGISTRY.write().unwrap();
217 if let Some(old) = registry.remove(block_id) {
218 let _ = old.stop(true, STATUS_DONE);
220 }
221 registry.insert(block_id.to_string(), controller);
222}
223
224pub fn delete_controller(block_id: &str) {
227 let ctrl = CONTROLLER_REGISTRY.write().unwrap().remove(block_id);
228 if let Some(ctrl) = ctrl {
229 let _ = ctrl.stop(true, STATUS_DONE);
230 }
231 if let Some(registry) = crate::backend::process_tracker::registry::global() {
236 registry.remove(block_id);
237 }
238}
239
240pub fn get_all_controllers() -> HashMap<String, Arc<dyn Controller>> {
242 CONTROLLER_REGISTRY.read().unwrap().clone()
243}
244
245#[allow(dead_code)]
247pub fn stop_all_controllers() {
248 let controllers = get_all_controllers();
249 for (_, ctrl) in controllers {
250 let _ = ctrl.stop(true, STATUS_DONE);
251 }
252}
253
254pub fn get_block_controller_status(block_id: &str) -> Option<BlockControllerRuntimeStatus> {
259 get_controller(block_id).map(|c| c.get_runtime_status())
260}
261
262#[allow(dead_code)]
264pub fn stop_block_controller(block_id: &str) -> Result<(), String> {
265 match get_controller(block_id) {
266 Some(ctrl) => ctrl.stop(true, STATUS_DONE),
267 None => Ok(()), }
269}
270
271pub fn send_input(block_id: &str, input: BlockInputUnion, seq: Option<u64>) -> Result<(), String> {
273 match get_controller(block_id) {
274 Some(ctrl) => ctrl.send_input(input, seq),
275 None => Err(format!("no controller for block {block_id}")),
276 }
277}
278
279pub enum AgentDelivery {
281 Structured,
286 Pty,
290}
291
292pub fn deliver_agent_message(block_id: &str, message: &str) -> Result<AgentDelivery, String> {
307 let ctrl = get_controller(block_id)
308 .ok_or_else(|| format!("no controller for block {block_id}"))?;
309
310 if let Some(persistent_ctrl) = ctrl
311 .as_any()
312 .downcast_ref::<persistent::PersistentSubprocessController>()
313 {
314 persistent_ctrl.send_user_message(message.to_string())?;
315 return Ok(AgentDelivery::Structured);
316 }
317
318 if ctrl.controller_type() == BLOCK_CONTROLLER_ACP {
319 ctrl.send_input(BlockInputUnion::data(message.as_bytes().to_vec()), None)?;
320 return Ok(AgentDelivery::Structured);
321 }
322
323 Ok(AgentDelivery::Pty)
324}
325
326pub fn resync_controller(
336 block: &Block,
337 tab_id: &str,
338 rt_opts: Option<serde_json::Value>,
339 force: bool,
340 broker: Option<Arc<Broker>>,
341 event_bus: Option<Arc<EventBus>>,
342 wstore: Option<Arc<Store>>,
343 filestore: Option<Arc<FileStore>>,
344) -> Result<(), String> {
345 let block_id = &block.oid;
346 let block_meta = &block.meta;
347
348 let controller_type = super::obj::meta_get_string(block_meta, META_KEY_CONTROLLER, "");
350
351 if controller_type.is_empty() {
352 return Ok(());
354 }
355
356 tracing::info!(
357 block_id = %block_id,
358 controller_type = %controller_type,
359 wstore_present = wstore.is_some(),
360 event_bus_present = event_bus.is_some(),
361 force,
362 "[dnd-debug] resync_controller entry"
363 );
364
365 let existing = get_controller(block_id);
367 if let Some(ref ctrl) = existing {
368 let needs_replace = if ctrl.controller_type() != controller_type || force {
369 true } else {
371 let status = ctrl.get_runtime_status();
372 let new_conn =
374 super::obj::meta_get_string(block_meta, META_KEY_CONNECTION, "local");
375 status.shellprocconnname != new_conn
376 };
377
378 if needs_replace {
379 let _ = ctrl.stop(true, STATUS_DONE);
380 delete_controller(block_id);
381 } else {
382 let status = ctrl.get_runtime_status();
384 tracing::info!(
385 block_id = %block_id,
386 status = %status.shellprocstatus,
387 "[dnd-debug] existing controller — skipping spawn (no cmd:cwd seed)"
388 );
389 if status.shellprocstatus == STATUS_INIT || status.shellprocstatus == STATUS_DONE {
390 return ctrl.start(block_meta.clone(), rt_opts, force);
391 }
392 return Ok(());
393 }
394 }
395
396 match controller_type.as_str() {
398 BLOCK_CONTROLLER_SHELL | BLOCK_CONTROLLER_CMD => {
399 let ctrl = shell::ShellController::new(
400 controller_type.clone(),
401 tab_id.to_string(),
402 block_id.to_string(),
403 broker,
404 event_bus,
405 wstore,
406 );
407 let ctrl = Arc::new(ctrl);
408 register_controller(block_id, ctrl.clone());
409 ctrl.start(block_meta.clone(), rt_opts, force)
410 }
411 BLOCK_CONTROLLER_SUBPROCESS => {
412 let ctrl = subprocess::SubprocessController::new(
413 tab_id.to_string(),
414 block_id.to_string(),
415 broker,
416 event_bus,
417 wstore,
418 filestore,
419 );
420 let ctrl = Arc::new(ctrl);
421 ctrl.set_self_ref();
422 register_controller(block_id, ctrl.clone());
423 ctrl.start(block_meta.clone(), rt_opts, force)
424 }
425 BLOCK_CONTROLLER_PERSISTENT => {
426 let ctrl = persistent::PersistentSubprocessController::new(
427 tab_id.to_string(),
428 block_id.to_string(),
429 broker,
430 event_bus,
431 wstore,
432 filestore,
433 );
434 let ctrl = Arc::new(ctrl);
435 register_controller(block_id, ctrl.clone());
436 ctrl.start(block_meta.clone(), rt_opts, force)
437 }
438 BLOCK_CONTROLLER_ACP => {
439 let ctrl = acp::AcpController::new(
440 tab_id.to_string(),
441 block_id.to_string(),
442 broker,
443 event_bus,
444 wstore,
445 filestore,
446 );
447 let ctrl = Arc::new(ctrl);
448 register_controller(block_id, ctrl.clone());
449 ctrl.start(block_meta.clone(), rt_opts, force)
450 }
451 BLOCK_CONTROLLER_TSUNAMI => {
452 Err("tsunami controller not yet implemented".to_string())
454 }
455 _ => Err(format!("unknown controller type: {controller_type}")),
456 }
457}
458
459pub fn publish_controller_status(
461 broker: &super::wps::Broker,
462 status: &BlockControllerRuntimeStatus,
463) {
464 use super::wps::{WaveEvent, EVENT_CONTROLLER_STATUS};
465
466 let event = WaveEvent {
467 event: EVENT_CONTROLLER_STATUS.to_string(),
468 scopes: vec![format!("block:{}", status.blockid)],
469 sender: String::new(),
470 persist: 0,
471 data: serde_json::to_value(status).ok(),
472 };
473 broker.publish(event);
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn test_status_constants() {
482 assert_eq!(STATUS_INIT, "init");
483 assert_eq!(STATUS_RUNNING, "running");
484 assert_eq!(STATUS_DONE, "done");
485 }
486
487 #[test]
488 fn test_controller_type_constants() {
489 assert_eq!(BLOCK_CONTROLLER_SHELL, "shell");
490 assert_eq!(BLOCK_CONTROLLER_CMD, "cmd");
491 assert_eq!(BLOCK_CONTROLLER_TSUNAMI, "tsunami");
492 }
493
494 #[test]
495 fn test_meta_key_constants() {
496 assert_eq!(META_KEY_CONTROLLER, "controller");
497 assert_eq!(META_KEY_CONNECTION, "connection");
498 assert_eq!(META_KEY_CMD, "cmd");
499 assert_eq!(META_KEY_CMD_RUN_ON_START, "cmd:runonstart");
500 }
501
502 #[test]
503 fn test_block_input_union_data() {
504 let input = BlockInputUnion::data(b"hello".to_vec());
505 assert_eq!(input.input_data.as_ref().unwrap(), b"hello");
506 assert!(input.sig_name.is_none());
507 assert!(input.term_size.is_none());
508 }
509
510 #[test]
511 fn test_block_input_union_signal() {
512 let input = BlockInputUnion::signal("SIGTERM");
513 assert!(input.input_data.is_none());
514 assert_eq!(input.sig_name.as_ref().unwrap(), "SIGTERM");
515 assert!(input.term_size.is_none());
516 }
517
518 #[test]
519 fn test_block_input_union_resize() {
520 let size = TermSize { rows: 40, cols: 120 };
521 let input = BlockInputUnion::resize(size.clone());
522 assert!(input.input_data.is_none());
523 assert!(input.sig_name.is_none());
524 let ts = input.term_size.unwrap();
525 assert_eq!(ts.rows, 40);
526 assert_eq!(ts.cols, 120);
527 }
528
529 #[test]
530 fn test_runtime_status_default() {
531 let status = BlockControllerRuntimeStatus::default();
532 assert!(status.blockid.is_empty());
533 assert_eq!(status.version, 0);
534 assert!(status.shellprocstatus.is_empty());
535 assert_eq!(status.shellprocexitcode, 0);
536 }
537
538 #[test]
539 fn test_runtime_status_serde() {
540 let status = BlockControllerRuntimeStatus {
541 blockid: "block-123".to_string(),
542 version: 3,
543 shellprocstatus: STATUS_RUNNING.to_string(),
544 shellprocconnname: "local".to_string(),
545 shellprocexitcode: 0,
546 ..Default::default()
547 };
548 let json = serde_json::to_string(&status).unwrap();
549 assert!(json.contains("\"blockid\":\"block-123\""));
550 assert!(json.contains("\"shellprocstatus\":\"running\""));
551
552 let parsed: BlockControllerRuntimeStatus = serde_json::from_str(&json).unwrap();
553 assert_eq!(parsed.blockid, "block-123");
554 assert_eq!(parsed.version, 3);
555 }
556
557 #[test]
558 fn test_get_nonexistent_controller() {
559 assert!(get_controller("nonexistent-block").is_none());
560 }
561
562 #[test]
563 fn test_get_block_controller_status_none() {
564 assert!(get_block_controller_status("nonexistent").is_none());
565 }
566
567 #[test]
568 fn test_stop_nonexistent_controller() {
569 assert!(stop_block_controller("nonexistent").is_ok());
571 }
572
573 #[test]
574 fn test_send_input_no_controller() {
575 let result = send_input("nonexistent", BlockInputUnion::data(b"test".to_vec()), None);
576 assert!(result.is_err());
577 assert!(result.unwrap_err().contains("no controller"));
578 }
579
580 #[test]
581 fn test_resync_no_controller_type() {
582 let block = Block {
583 oid: "test-block".to_string(),
584 version: 1,
585 meta: HashMap::new(),
586 ..Default::default()
587 };
588 let result = resync_controller(&block, "tab-1", None, false, None, None, None, None);
590 assert!(result.is_ok());
591 }
592
593 #[test]
594 fn test_resync_unknown_controller_type() {
595 let mut meta = MetaMapType::new();
596 meta.insert(
597 "controller".to_string(),
598 serde_json::Value::String("unknown_type".to_string()),
599 );
600 let block = Block {
601 oid: "test-block".to_string(),
602 version: 1,
603 meta,
604 ..Default::default()
605 };
606 let result = resync_controller(&block, "tab-1", None, false, None, None, None, None);
607 assert!(result.is_err());
608 assert!(result.unwrap_err().contains("unknown controller type"));
609 }
610}